延續上一篇的內容,本文繼續跟大家分享一些正確觀念,為了就是在開發時可以撰寫出品質好的程式碼,寫的任何一個元件都很重要,它們為何存在,使用的優缺點是什麼,都應該清楚了解。如果是以好習慣寫出來的畫面,當然除了性能表現佳之外,後續需要解決的問題也會變少,是很值得前期重視的投資哦!
saveLayer()
也稱為離屏渲染,是引擎在針對某些情境處理渲染時會使用到的操作,本身屬於高成本、高耗時。螢幕外緩衝區(off-screen buffer)
,並且將內容繪製到這裡,GPU 處理時會進行渲染目標的轉換,跳轉到另一個 Layer,導致運行緩慢****disabledColorAlpha ≠ 0xff
overflowShader
提醒:可使用 Skia Screenshot 協助我們檢查渲染過程,詳細的說明可閱讀另一篇文章(等待上傳)
withOpacity()
方法來添加不透明層Container(color: Colors.blue.withOpacity(0.5)),
ColoredBox(color: Colors.blue.withOpacity(0.6)),
Text('Hi!', style: TextStyle(color: Colors.blue.withOpacity(1))),
// Image
Image.network(
'https://storage.googleapis.com/cms-storage-bucket/70760bf1e88b184bb1bc.png',
opacity: _animationController,
),
Image.network(
'https://storage.googleapis.com/cms-storage-bucket/70760bf1e88b184bb1bc.png',
opacity: AlwaysStoppedAnimation(_animationController.value),
),
saveLayer()
,不會跟 Opacity ****一樣麻煩,但還是有成本,默認情況下裁剪被禁用為 Clip.none
,除非使用 Clip.antiAliasWithSaveLayer
borderRadius
屬性去實現,而不要使用 ClipRRect 裁切矩形,實際上圖形引擎在處理的過程會比較輕鬆,性能比較好Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
checkerboardOffscreenLayers
為 truesaveLayer()
相關操作,有的話會透過棋盤格呈現RepaintBoundary
包裹不需要更新的元件,例如:動畫元件display list(一連串輸出圖像的命令)
,包含許多元件,可以將主要渲染元件與其他元件分割,不同 Layer 的分離,實現只繪製內容發生變化的 subtree
,告訴 Flutter 這些元件應該在不同的 WidgetTree,處理自己的繪製,不會被其他不相關的繪製影響markNeedsPaint()
和 paintChild()
的使用,避免同一層的相關 RenderObject 被重新繪製。通常只要child.isRepaintBoundary
為 false,那麼就會執行 paint
方法,重新繪製子元件注意:Raster Cache 光柵緩存,創建成本高,會佔用大量 GPU 效能,濫用會造成過多的記憶體使用,因為需要緩存更多資訊
在 main 可以設置 debugRepaintRainbowEnabled
為 true,畫面會將元件透過顏色線條框起來,可幫助發現正在被繪製的區塊,有刷新的話線條顏色會一直變換。
checkerboardRasterCacheImages
為 trueImageFilter
的渲染速度更快RepaintBoundry
,以減少重新渲染模糊效果的頻率// Bad. BackdropFilter
Stack(
children: [
Image.asset(
'https://storage.googleapis.com/cms-storage-bucket/70760bf1e88b184bb1bc.png',
width: 50,
height: 50,
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(
color: Colors.grey.withOpacity(0.6),
),
),
],
),
// Good. ImageFiltered
Container(
color: Colors.blue.withOpacity(0.5),
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Image.asset(
'https://storage.googleapis.com/cms-storage-bucket/70760bf1e88b184bb1bc.png',
width: 50,
height: 50,
),
),
),
如果是一般 TabBar 和 TabView,它會在首次加載時實體化所有頁面、元件,而對於多個頁面的情境,這可能會導致一開始載入或是跳轉的過渡動畫有嚴重卡頓,幀數過低。
以下範例的寫法可以只載入需要顯示的畫面元件,減少資源的消耗:
late final List<bool> _pageActivateds = List<bool>.filled(4, false);
Widget build(BuildContext context) {
_pageActivateds[widget.index] = true;
final children = List.generate(
_pageActivateds.length,
(i) => _pageActivateds[i] ? pages[i] : const SizedBox(),
);
return IndexedStack(children: children);
}
// FROM
final complextWidget = Container();
return isGood ? Container(child: complextWidget) : complextWidget,
// TO
final complextWidgetKey = GlobalKey();
final complextWidget = Container(key: complextWidgetKey);
return isGood ? Container(child: complextWidget) : complextWidget,
提醒:使用 GlobalKey 這個過程也稱為 Tree Sugery,很方便但也很危險,使用成本高,濫用的情況下會很大的影響效能,很可能會造成卡頓、不順暢
super.dispose()
需作為最後一個執行函式,資源釋放需要在之前執行,如果在後面則不會處理@override
void dispose() {
_animationController.dispose();
super.dispose();
}
進行數據壓縮後可有效節省記憶體,需要時再解壓縮並使用。如果需要將某些資料儲存到本地,例如:文件、SharedPreference 等等,可以先將資料壓縮後再儲存。不過,還是根據實際場景決定是否真的需要。
final jsonString = await rootBundle.loadString('assets/food.json');
// compressed
final original = utf8.encode(jsonString);
final compressed = gzip.encode(original);
// decompressed
final decompressed = gzip.decode(compressed);
final jsonString = utf8.decode(decompressed);
當此任務在同步和非同步處理時可能會花很長且不可預期的時間,或是跟原生平台的互動,都適合使用 Isolate,例如:大量的文字解析、圖像處理、大型檔案的存取,都可能會堵塞 Main Isolate,影響到 Rendering Pipeline。適當地使用 Isolate 可以提升性能並優化整體的使用者體驗。
以下是簡易範例:
void createIsolate() async {
Isolate? isolate;
ReceivePort receivePort = ReceivePort();
try {
isolate = await Isolate.spawn(_worker, receivePort.sendPort);
receivePort.listen((dynamic message) {
debugPrint(message.toString());
});
} catch (e) {
debugPrint(e.toString());
} finally {
isolate?.addOnExitListener(receivePort.sendPort, response: "isolate has been killed");
}
isolate?.kill();
}
void _worker(SendPort sendPort) {
// Do complex task..
}
詳細的 Isolate 說明我有在另一篇文章提及,有興趣請點擊連結深入了解。
Day 10: Async 和 Isolates 差異在哪裡?正確使用才能確保流暢體驗!
不要忘記持續追蹤 Flutter Repo,在每個版本中,Flutter 都進行了許多優化,以現在來看,近期都在處理 Dart 3、Impeller Renderer、Material 3、Widget API、Document,當然也包含很多原生平台的更新,效能持續提升,以整體來看大部分情境下是好處多的。同時呼籲大家更新後,如果有發現任何 Bug 問題,都不要吝嗇的發 Issue 到 Flutter Repo,大家的積極只會讓 Flutter 進步更快,我們共勉之。
flutter upgrade
本文的 saveLayer 與 RepaintBoundary 操作都是 UI 的開發重點,如何以性能較好的方式去撰寫,需要養成習慣。而除了上述提到的內容大部分都與 UI 有關,也包含資料處理和 Isolate 使用,建議有時間的話,可以嘗試理解每個開發選擇和優化的由來,相信對於未來實作上的理解會更不一樣。
對於優化與觀念部分,喜歡的話請留言讓我知道,會在整理一些能幫助大家的內容。本系列已經到一半了,不覺得時間過很快嗎,休息放鬆一下,我們繼續加油!